package com.google.codejam.core.test;

import com.google.codejam.core.io.TestCaseInputReader;
import com.google.codejam.core.io.TestCaseOutputWriter;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * Abstract base class for Code Jam Test Suite Runner implementations.
 */
public abstract class ACodeJamTestSuiteRunner
{
	private static final SimpleDateFormat DATE_FORMAT =
			  new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

	private String input_path;
	private String output_path;
	private String test_suite_name;

	protected TestCaseInputReader  input_reader;
	protected TestCaseOutputWriter output_writer;

	protected int                    test_case_count;
	protected List<ACodeJamTestCase> test_case_list;

	/**
	 * Constructor that assigns the test suite name.
	 *
	 * @param test_suite_name
	 * 		  <code>String</code> containing the name of the test suite.
	 */
	public ACodeJamTestSuiteRunner(String test_suite_name)
	{
		this.test_suite_name = test_suite_name;
	}

	/**
	 * Performs various steps required to run through the test suite.
	 * <p>The steps performed are:
	 *    <ol>
	 *       <li>Initialize the test suite - <code>init</code> method.</li>
	 *       <li>Read test cases from input - <code>readTestCasesFromInput</code> method.</li>
	 *       <li>Solve test cases - <code>solveTestCases</code> method.</li>
	 *       <li>Write results to output - <code>writeResultsToOutput</code> method.</li>
	 *    </ol>
	 * </p>
	 * <p>If an exception is thrown at any of the aforementioned steps, further
	 * processing is aborted.</p>
	 *
	 * @param args <code>String[]</code> containing the arguments required for
	 * initialization.
	 */
	public final void runTestSuite(String[] args)
	{
		printToOutput("Starting initialization for test suite: " +
				                test_suite_name + ".");

		try
		{
			init(args);
		}
		catch (Exception err)
		{
			printToOutput("Exception occurred during initialization: " +
					                err.toString());
			err.printStackTrace();
			return;
		}

		printToOutput("Initialization complete.");

		printToOutput("Reading test cases from input: " + input_path);

		try
		{
			readTestCasesFromInput();
		}
		catch (IOException err)
		{
			printToOutput(
					  "IOException occurred while reading test case input from file: " +
								 err.toString());
			err.printStackTrace();
			return;
		}

		printToOutput("Successfully read test cases from input.");

		printToOutput("Solving test cases.");

		try
		{
			solveTestCases();
		}
		catch (Exception err)
		{
			printToOutput(
					  "Exception occurred while solving test cases: " +
								 err.toString());
			err.printStackTrace();
			return;
		}

		printToOutput("Test case solutions calculated.");

		printToOutput("Writing results to output file: " + output_path);

		try
		{
			writeResultsToOutput();
		}
		catch (IOException err)
		{
			printToOutput(
					  "IOException occurred while writing test case results to the " +
								 "output file: " + err.getMessage());
			return;
		}

		printToOutput("Done writing results to output file.");

		printToOutput("Test suite complete: " + test_suite_name + ".");
	}

	/**
	 * This method uses the input parameters (<code>String]\</code> args) to
	 * initialize this test suite runner instance.
	 * <p>
	 *    The following actions are performed to facilitate initialization:
	 *    <ol>
	 *       <li>Initialize input parameters - <code>initInputParams</code> method.</li>
	 *       <li>Initialize input and output - <code>initIO</code> method.</li>
	 *       <li>Initialize list of test cases - <code>initTestCaseList</code> method.</li>
	 *    </ol>
	 * </p>
	 *
	 * @param args <code>String[]</code> containing the parameters used for
	 * initialization.
	 * @throws IOException
	 */
	protected void init(String[] args)
			  throws IOException
	{
		initInputParams(args);
		initIO();
		initTestCaseList();
	}

	/**
	 * Parses the input parameters to obtain the input- and output paths,
	 * respectively.
	 *
	 * @param input_params <code>String[]</code> containing the input parameters.
	 */
	protected void initInputParams(String[] input_params)
	{
		if (input_params == null || input_params.length != 2)
		{
			throw new IllegalArgumentException(
					  "Two mandatory parameters are required: <input path> and <output path>.");
		}

		input_path = input_params[0];
		output_path = input_params[1];
	}

	/**
	 * Initialize the input reader and output writer. The file paths are passed
	 * as part of the application parameters.
	 *
	 * @throws FileNotFoundException
	 */
	protected void initIO()
			  throws FileNotFoundException
	{
		FileInputStream file_in = new FileInputStream(input_path);
		input_reader = new TestCaseInputReader(file_in);

		FileOutputStream file_out = new FileOutputStream(output_path);
		output_writer = new TestCaseOutputWriter(file_out);
	}

	/**
	 * Instantiate the internal list of test cases. The initial capacity of the
	 * list is obtained by reading the first line of the input file. For this
	 * reason it is important that the input reader is initialized before this
	 * method is called.
	 *
	 * @throws IOException
	 */
	protected void initTestCaseList()
			  throws IOException
	{
		test_case_count = Integer.parseInt(input_reader.readLine());
		test_case_list = new ArrayList<ACodeJamTestCase>(test_case_count);
	}

	/**
	 * Reads test cases from the input. The number of test cases is determined
	 * from the first line of the input file. This value is used as a counter
	 * for a loop to read all the test cases from the input. The test case
	 * implementations (<code>ACodeJamTestCase</code> implementations) are added
	 * to an internal list.
	 *
	 * @throws IOException
	 */
	private void readTestCasesFromInput()
			  throws IOException
	{
		try
		{
			for (int i = 0; i < test_case_count; i++)
			{
				test_case_list.add(i, initNextTestCaseFromInput());
			}
		}
		finally
		{
			input_reader.close();
		}
	}

	/**
	 * This method iterates though the internal list of test cases and calls the
	 * <code>solve</code> method of each test case.
	 */
	protected void solveTestCases()
	{
		for (ISolvable solvable : test_case_list)
		{
			solvable.solve();
		}
	}

	/**
	 * This method iterates through the internal list of test cases and writes
	 * the result of each test case (with the test case count) to the output.
	 * <p>This method should be called after the test cases have been solved, to
	 * avoid <code>null</code> values being written to the output.</p>
	 *
	 * @throws IOException
	 */
	protected void writeResultsToOutput()
			  throws IOException
	{
		try
		{
			for (ISolvable solvable : test_case_list)
			{
				output_writer.writeLine(solvable.getResultString());
			}
		}
		finally
		{
			output_writer.close();
		}
	}

	/**
	 * Get the current test case counter value.
	 *
	 * @return <code>int</code> test case count.
	 */
	protected int getTestCaseCount()
	{
		return test_case_count;
	}

	/**
	 * Print the supplied line of text to the standard output. Also prepends the
	 * current time in the format: yyyy-MM-dd HH:mm:ss.SSS.
	 *
	 * @param line_to_print <code>String</code> line of text to print to the
	 * standard output.
	 */
	public static void printToOutput(String line_to_print)
	{
		String timeStamp = DATE_FORMAT.format(Calendar.getInstance().getTime());

		System.out.println(timeStamp + " > " + line_to_print);
	}

	/**
	 * Ensures the proper clean-up is performed, specifically the IO objects.
	 * Also calls <code>super.finalize()</code>.
	 *
	 * @throws Throwable
	 */
	public void finalize()
			  throws Throwable
	{
		if (input_reader != null)
		{
			input_reader.close();
		}

		if (output_writer != null)
		{
			output_writer.close();
		}

		super.finalize();
	}

	/**
	 * Reads from the input file the data required, and performs the necessary
	 * processing to instantiate an instance of an <code>ACodeJamTestCase</code>
	 * implementation.
	 *
	 * @return <code>ACodeJamTestCase</code> implementation to be added to the
	 * internal list of test cases.
	 * @throws IOException
	 */
	protected abstract ACodeJamTestCase initNextTestCaseFromInput()
			  throws IOException;
}
